
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: Index.java 380 2010-03-11 22:27:58Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.dellroad.sidekar.query.QueryIterable;
import org.dellroad.sidekar.query.QueryIterator;
import org.dellroad.sidekar.query.QuerySet;

/**
 * An index on one or more fields of an {@link Entity}.
 *
 * <p>
 * An {@link Index} is associated with an {@link Entity} and one or more fields within it. Given candidate values for
 * those fields, the index efficiently locates all instances of the {@link Entity} having the given values in the
 * corresponding fields.
 * </p>
 *
 * <p>
 * When querying an index, it is not required that all of the fields' values be given. Only some of the values may be given,
 * in which case the returned instances can have any value in the remaining unspecified fields.
 * </p>
 *
 * <p>
 * The matching instances are returned in a {@link QueryIterable}, and they will be sorted by their values in the
 * unspecified field(s), if any. They will be sorted first by the value in the first unspecified field,
 * then by the value in the second unspecified field, and so on. See the Javadocs for {@linkplain FieldType the various
 * field types} for information on how each field type sorts.
 * </p>
 *
 * <p>
 * As a result, an {@link Index} can also be used simply for sorting. For example, an {@link Index} on field {@code foo}
 * can be used to retrieve all instances sorted by {@code foo}, by querying the {@link Index} without specifying
 * a value (for example, using {@link #iterator() Index.iterator()}). To get the reversed sort, use
 * {@link #descendingIterator() Index.descendingIterator()}).
 * When not otherwise sorted, {@link Item}s will iterate consistently with {@link Entity#iterator(boolean) Entity.iterator()}.
 * </p>
 *
 * <p>
 * In the case where all of the fields values are specified, a {@link org.dellroad.sidekar.query.QueryIterator} can be
 %%
 * obtained, which means that it can be efficiently "joined" with any other {@link org.dellroad.sidekar.util.JumpIterator}
 * using a {@link org.dellroad.sidekar.util.JumpJoiner}. For example, suppose a class {@code Family} has a
 * field of type {@code Set<Person>}, and the {@code Person} class has a String field called "name" and an index
 * on that field named "byName". To find all {@code Person} instances in a family's {@code Set<Person>} also having
 * name {@code "fred"}, join the {@link org.dellroad.sidekar.util.JumpIterator} returned by the set with the
 * {@link org.dellroad.sidekar.util.JumpIterator} returned by the "byName" index when given the value {@code "fred"}.
 * </p>
 *
 * <p>
 * Only non-explicit field types that subclass {@link org.dellroad.sidekar.SimpleField} are indexable (basically,
 * any field type except collection types and {@linkplain ImplicitField implicit fields}).
 * </p>
 *
 * @param T user model class associated with the associated entity
 * @see org.dellroad.sidekar.util.JumpIterator
 * @see org.dellroad.sidekar.util.JumpJoiner
 * @see FieldType
 * @see org.dellroad.sidekar.annotation.SidekarIndex
 * @see org.dellroad.sidekar.util.ReverseCursorIterator
 * @see org.dellroad.sidekar.util.ReverseCursorJumpIterator
 */
public class Index<T> extends SchemaEntityElement {

    // Index information
    private final String[] fieldNames;
    private final boolean unique;

    /**
     * Constructor.
     *
     * @throws IllegalArgumentException if any field name is not found in the target entity
     */
    Index(Entity<T> entity, String name, List<String> fieldNames, boolean unique) {
        super(entity, name);
        Sidekar.checkValidIndexName(name);
        if (fieldNames.isEmpty())
            throw new IllegalArgumentException();
        this.fieldNames = fieldNames.toArray(new String[fieldNames.size()]);
        this.unique = unique;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Entity<T> getEntity() {
        return (Entity<T>)super.getEntity();
    }

    /**
     * Get the names of the indexed fields.
     */
    public String[] getFieldNames() {
        return this.fieldNames.clone();
    }

    /**
     * Get whether this index is unique.
     */
    public boolean isUnique() {
        return this.unique;
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder("index \"" + this.getName() + "\" on fields ");
        boolean first = true;
        for (String fieldName : this.fieldNames) {
            if (first)
                first = false;
            else
                buf.append(", ");
            buf.append("\"")
              .append(fieldName)
              .append("\"");
        }
        return buf.toString();
    }

    /**
     * Query this index for all items having the given values.
     *
     * <p>
     * If the number of {@code values} is less than the number of fields in this index, then
     * the items will be sorted according to the values in the unspecified fields. They will
     * be sorted first by the value in the first unspecified field, then by the value in the
     * next unspecified field, and so on. In particular, if no values are given, then all
     * instances of the associated {@link Entity} are returned and they will be sorted according
     * to the values in this {@link Index}'s fields.
     * </p>
     *
     * <p>
     * As a special case, if the last specified field is a {@link StringField}, then a {@link StringPrefix}
     * value may be supplied in place of a {@link String} value. This will match all strings having the given
     * prefix and sort them by their remaining suffixes.
     * </p>
     *
     * <p>
     * The returned iterator supports the {@link java.util.Iterator#remove remove()} method; when
     * invoked, the current item will be detached as if by {@link Item#sidekarDetach}.
     * </p>
     *
     * @param values values for none, some, or all of the indexed fields
     * @throws IllegalArgumentException if the size of {@code values} is greater than the number of fields in this index
     * @throws IllegalArgumentException if any value has a type that's incompatible with the corresponding field
     * @throws IllegalStateException if there is no {@link org.dellroad.sidekar.util.CloseableNotes}
     *  associated with the current thread
     * @throws IllegalStateException if the database containing the associated entity has been closed
     * @see #jumpIterator(List)
     */
    public QuerySet<T> query(List<?> values) {
        this.validate();
        return this.getDatastore().indexAccess(this.getEntity(), this, values);
    }

    /**
     * Convenience method. Equivalent to:
     *  <blockquote>
     *  {@code iterator(Collections.emptyList())}
     *  </blockquote>
     *
     * @see #iterator(List)
     * @see #jumpIterator()
     */
    public QuerySet<T> query() {
        return this.query(Collections.emptyList());
    }

    /**
     * Convenience method. Equivalent to:
     *  <blockquote>
     *  {@code iterator(Arrays.asList(objs))}
     *  </blockquote>
     *
     * @see #iterator(List)
     * @see #jumpIterator(Object...)
     */
    public QuerySet<T> query(Object... values) {
        return this.query(Arrays.asList(values));
    }

    /**
     * Convenience method. Returns the first {@link Item} in the index, or {@code null} if there is none.
     *
     * @throws IllegalStateException if the database containing the associated entity has been closed
     * @see #iterator(List)
     */
    public T getFirst(final List<?> values) {
        QueryIterator<T> i = this.query(values).iterator();
        try {
            return i.hasNext() ? i.next() : null;
        } finally {
            i.close();
        }
    }

    /**
     * Convenience method. Equivalent to:
     *  <blockquote>
     *  {@code getFirst(Arrays.asList(objs))}
     *  </blockquote>
     *
     * @throws IllegalStateException if the database containing the associated entity has been closed
     * @see #getFirst(List)
     */
    public T getFirst(Object... values) {
        return this.getFirst(Arrays.asList(values));
    }
}

